{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# LNA Example\n",
    "\n",
    "Let's design a LNA using Infineon's BFU520 transistor. First we need to import scikit-rf and a bunch of other utilities:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "import skrf as rf\n",
    "from skrf.media import DistributedCircuit\n",
    "\n",
    "plt.rcParams['figure.figsize'] = [10, 10]\n",
    "\n",
    "f = rf.Frequency(0.4, 2, 101, 'GHz')\n",
    "tem = DistributedCircuit(f, z0=50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# import the scattering parameters/noise data for the transistor\n",
    "bjt = rf.Network('BFU520_05V0_010mA_NF_SP.s2p').interpolate(f)\n",
    "print(bjt)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's plot the smith chart for it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bjt.plot_s_smith(lw=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's calculate the source and load stability curves.\n",
    "I'm slightly misusing the `Network` type to plot the curves; normally the curves you pass in to `Network` should be a function of frequency, but it also works to draw these circles as long as you don't try to use any other functions on them"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sqabs = lambda x: np.square(np.absolute(x))  # noqa: E731\n",
    "\n",
    "delta = bjt.s11.s*bjt.s22.s - bjt.s12.s*bjt.s21.s\n",
    "rl = np.absolute((bjt.s12.s * bjt.s21.s)/(sqabs(bjt.s22.s) - sqabs(delta)))\n",
    "cl = np.conj(bjt.s22.s - delta*np.conj(bjt.s11.s))/(sqabs(bjt.s22.s) - sqabs(delta))\n",
    "\n",
    "rs = np.absolute((bjt.s12.s * bjt.s21.s)/(sqabs(bjt.s11.s) - sqabs(delta)))\n",
    "cs = np.conj(bjt.s11.s - delta*np.conj(bjt.s22.s))/(sqabs(bjt.s11.s) - sqabs(delta))\n",
    "\n",
    "def calc_circle(c, r):\n",
    "    theta = np.linspace(0, 2*np.pi, 1000)\n",
    "    return c + r*np.exp(1.0j*theta)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i, f in enumerate(bjt.f):\n",
    "    # decimate it a little\n",
    "    if i % 100 != 0:\n",
    "        continue\n",
    "    n = rf.Network(name=str(f/1.e+9), s=calc_circle(cs[i][0, 0], rs[i][0, 0]))\n",
    "    n.plot_s_smith()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i, f in enumerate(bjt.f):\n",
    "    # decimate it a little\n",
    "    if i % 100 != 0:\n",
    "        continue\n",
    "    n = rf.Network(name=str(f/1.e+9), s=calc_circle(cl[i][0, 0], rl[i][0, 0]))\n",
    "    n.plot_s_smith()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So we can see that we need to avoid inductive loads near short circuit in the input matching network and high impedance inductive loads on the output.\n",
    "\n",
    "Let's draw some constant noise circles. First we grab the noise parameters for our target frequency from the network model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "idx_915mhz = rf.util.find_nearest_index(bjt.f, 915.e+6)\n",
    "\n",
    "# we need the normalized equivalent noise and optimum source coefficient to calculate the constant noise circles\n",
    "rn = bjt.rn[idx_915mhz]/50\n",
    "gamma_opt = bjt.g_opt[idx_915mhz]\n",
    "fmin = bjt.nfmin[idx_915mhz]\n",
    "\n",
    "for nf_added in [0, 0.1, 0.2, 0.5]:\n",
    "    nf = 10**(nf_added/10) * fmin\n",
    "\n",
    "    N = (nf - fmin)*abs(1+gamma_opt)**2/(4*rn)\n",
    "    c_n = gamma_opt/(1+N)\n",
    "    r_n = 1/(1-N)*np.sqrt(N**2 + N*(1-abs(gamma_opt)**2))\n",
    "\n",
    "    n = rf.Network(name=str(nf_added), s=calc_circle(c_n, r_n))\n",
    "    n.plot_s_smith()\n",
    "\n",
    "print(\"the optimum source reflection coefficient is \", gamma_opt)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So we can see from the chart that just leaving the input at 50 ohms gets us under 0.1 dB of extra noise, which seems pretty good. I'm actually not sure that these actually correspond to the noise figure level increments I have listed up there, but the circles should at least correspond to increasing noise figures\n",
    "\n",
    "So let's leave the input at 50 ohms and figure out how to match the output network to maximize gain and stability. Let's see what matching the load impedance with an unmatched input gives us:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gamma_s = 0.0\n",
    "\n",
    "gamma_l = np.conj(bjt.s22.s - bjt.s21.s*gamma_s*bjt.s12.s/(1-bjt.s11.s*gamma_s))\n",
    "gamma_l = gamma_l[idx_915mhz, 0, 0]\n",
    "is_gamma_l_stable = np.absolute(gamma_l - cl[idx_915mhz]) > rl[idx_915mhz]\n",
    "\n",
    "gamma_l, is_gamma_l_stable"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This looks like it may be kind of close to the load instability circles, so it might make sense to pick a load point with less gain for more stability, or to pick a different source impedance with more noise.\n",
    "\n",
    "But for now let's just build a matching network for this and see how it performs:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def calc_matching_network_vals(z1, z2):\n",
    "    flipped = np.real(z1) < np.real(z2)\n",
    "    if flipped:\n",
    "        z2, z1 = z1, z2\n",
    "\n",
    "    # cancel out the imaginary parts of both input and output impedances\n",
    "    z1_par = 0.0\n",
    "    if abs(np.imag(z1)) > 1e-6:\n",
    "        # parallel something to cancel out the imaginary part of\n",
    "        # z1's impedance\n",
    "        z1_par = 1/(-1j*np.imag(1/z1))\n",
    "        z1 = 1/(1./z1 + 1/z1_par)\n",
    "    z2_ser = 0.0\n",
    "    if abs(np.imag(z2)) > 1e-6:\n",
    "        z2_ser = -1j*np.imag(z2)\n",
    "        z2 = z2 + z2_ser\n",
    "\n",
    "    Q = np.sqrt((np.real(z1) - np.real(z2))/np.real(z2))\n",
    "    x1 = -1.j * np.real(z1)/Q\n",
    "    x2 = 1.j * np.real(z2)*Q\n",
    "\n",
    "    x1_tot = 1/(1/z1_par + 1/x1)\n",
    "    x2_tot = z2_ser + x2\n",
    "    if flipped:\n",
    "        return x2_tot, x1_tot\n",
    "    else:\n",
    "        return x1_tot, x2_tot\n",
    "\n",
    "z_l = rf.s2z(np.array([[[gamma_l]]]))[0,0,0]\n",
    "# note that we're matching against the conjugate;\n",
    "# this is because we want to see z_l from the BJT side\n",
    "# if we plugged in z the matching network would make\n",
    "# the 50 ohms look like np.conj(z) to match against it, so\n",
    "# we use np.conj(z_l) so that it'll look like z_l from the BJT's side\n",
    "z_par, z_ser = calc_matching_network_vals(np.conj(z_l), 50)\n",
    "z_l, z_par, z_ser"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's calculate what the component values are:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "c_par = np.real(1/(2j*np.pi*915e+6*z_par))\n",
    "l_ser = np.real(z_ser/(2j*np.pi*915e+6))\n",
    "\n",
    "print(c_par, l_ser)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The capacitance is kind of low but the inductance seems reasonable. Let's test it out:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "output_network = tem.shunt_capacitor(c_par) ** tem.inductor(l_ser)\n",
    "\n",
    "amplifier = bjt ** output_network\n",
    "\n",
    "amplifier.plot_s_smith()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That looks pretty reasonable; let's take a look at the S21 to see what we got:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "amplifier.s21.plot_s_db()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So about 18 dB gain; let's see what our noise figure is:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "10*np.log10(amplifier.nf(50.)[idx_915mhz])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So 0.96 dB NF, which is reasonably close to the BJT tombstone optimal NF of 0.95 dB"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
